package asteroids.model;

import java.util.HashSet;
import java.util.Set;

import asteroids.IShip;
import asteroids.Vector;
import asteroids.model.programs.Program;
import be.kuleuven.cs.som.annotate.*;

/**
 * A class of space ships involving position data, velocity, angle of direction, etc...
 * 
 * @version	1.0
 * @author	Ewoud Van Esch & Jomi Verwimp
 */
public class Ship extends GameObject implements IShip {
	
	/**
	 * Initialize this new ship with given x- & y-position, given x- & y-velocity, 
	 * given radius and given angle of direction.
	 * 
	 * @param	xPos
	 * 			The x-position for this ship expressed in km.
	 * @param	yPos
	 * 			The y-position for this ship expressed in km.
	 * @param	xVel
	 * 			The x-velocity for this ship expressed in km/s.
	 * @param	yVel
	 * 			The y-velocity for this ship expressed in km/s.
	 * @param	radius
	 * 			The radius for this ship expressed in km.
	 * @param	mass
	 * 			The mass for this ship expressed in kg.
	 * @param	angle
	 * 			The angle of direction for this ship expressed in radians.
	 * @pre		The given angle must be a valid angle for any ship.
	 * 		  | isValidAngle(angle)
	 * @post	The new radius for this new ship is equal to the given radius.
	 * 		  |	new.getRadius() == radius
	 * @post	The new angle of direction for this new ship is equal to
	 * 			the given angle.
	 * 		  |	new.getAngle() == angle
	 * @post	The new xPosition of this new ship is equal to the given x.
	 * 		  | new.getXPosition() == x
	 * @post	The new yPosition of this new ship is equal to the given y.
	 * 		  | new.getYPosition() == y
	 * @post	If the given x- and y-velocity are valid arguments, so that the speed
	 * 			limit is not breached, then the new xVelocity and yVelocity are set to
	 * 			the given arguments. Otherwise x- and y-velocity are lowered until
	 * 			the speed is lower than the speed limit.
	 * @throws	IllegalArgumentException("The given radius is not a valid radius!")
	 * 			This new ship cannot have the given radius as its radius.
	 * 		  | ! isValidRadius(radius)
	 */
	public Ship(double xPos, double yPos, double xVel, double yVel, double radius, double mass, double angle) {
		super(xPos, yPos, xVel, yVel, radius, mass);
		setAngle(angle);	
	}
	
	/**
	 * Accelerate this ship over the given time duration.
	 * 
	 * @param	deltaT
	 * 			Duration of time over which the ship must be accelerated.
	 * @effect	If and only if  this ships thruster is enabled,
	 * 			the new velocity is equal to the old velocity
	 * 			increased with an amount equal to the fraction 
	 * 			of the impulse of this ships thruster and its mass,
	 * 			with respect to the current angle of this ship.
	 * 		  | if (isThrusterActive() == true)
	 * 		  | 	then (
	 * 		  |        a = getThrustImpulse()*deltaT/getMass()
	 * 		  |        setVelocity( Vector.add(getVelocity(),a, getAngle()) ))
	 */
	public void thrust(double deltaT) {
		if (isThrusterActive() == true) {
			double a = (getThrustImpulse()*deltaT)/getMass();
			setVelocity( Vector.add(getVelocity(),a, getAngle()) );
		}
	}
	
	/**
	 * Return whether or not the thruster of this ship is active.
	 */
	@Basic
	public boolean isThrusterActive() {
		return thruster;
	}
	
	/**
	 * Set the thruster of this ship to the given state.
	 * 
	 * @param	state
	 * 			The new state for the thruster of this ship.
	 * @post	The new state of the thruster of this ship is equal to the given state.
	 * 		  | new.isThrusterActive() == state
	 */
	public void setThruster(boolean state) {
		this.thruster = state;
	}
	
	/**
	 * Boolean registering whether the thruster of this ship is active.
	 */
	private boolean thruster = false;
	
	/**
	 * Return the thrust impulse of this ship.
	 */
	@Basic @Immutable
	public double getThrustImpulse() {
		return thrustImpulse;
	}
	
	/**
	 * Variable holding the power output (impulse) of this ships thruster expressed in N/s.
	 */
	private final double thrustImpulse = 1.1E18;
	
	/**
	 * Return the current direction of this ship expressed as an angle in radians.
	 * 		The angle of a ship facing right is 0, a ship facing up is at angle Pi/2,
	 * 		a ship facing left is at angle Pi and a ship facing down is at angle (3*Pi)/2.
	 */
	@Basic @Raw
	public double getAngle() {
		return this.angle;
	}
	
	/**
	 * Rotate this ship over the given angle.
	 * 
	 * @param	angle
	 * 			The angle to rotate over expressed in radians.
	 * @pre		The given angle is not NaN and not infinite.
	 * 		  | (angle != NaN) && (angle != Infinity)
	 * @effect	The new angle of this ship is equal to
	 * 			the old angle of this ship rotated over the given angle.
	 * 		  | setAngle(getAngle()+angle)
	 */
	public void rotate(double angle) {
		Double angleObj = new Double(angle); 
		assert (!angleObj.isInfinite()) && (!angleObj.isNaN());
		double newAngle = (getAngle()+angle) % (2*Math.PI);
		setAngle(newAngle < 0 ? newAngle + 2*Math.PI : newAngle);
	}
	
	/**
	 * Set the angle of this ship to the given angle.
	 * 
	 * @param	angle
	 * 			The new angle for this ship expressed in radians.
	 * @pre		The given angle must lie between 0 and 2*PI.
	 * 		  | (angle >= 0) && (angle < 2*PI)
	 * @post	The new angle of this ship is equal to the given angle.
	 * 		  | new.getAngle() == angle
	 */
	@Raw
	private void setAngle(double angle) {
		assert (angle >= 0) && (angle < 2*Math.PI);
		this.angle = angle;
	}
	
	/**
	 * Variable registering the current direction of this ship.
	 */
	private double angle = 0;
	
	/**
	 * Fire bullets from this ship.
	 * 
	 * @effect	If and only if this ship has less than 3 live bullets, a new bullet 
	 * 			is made with the appropriate position, velocity, radius, source ship and world.
	 * 			The bullet is then added to the set of live bullets of this ship.
	 * 
	 * 		  | if(getLiveBullets().size() < 3)
	 * 		  |		then(
	 * 		  |			double x = getPosition().getX()+getRadius()*Math.cos(getAngle())
	 * 		  |			double y = getPosition().getY()+getRadius()*Math.sin(getAngle())
	 * 		  |			double xVel = getVelocity().getX()+250*Math.cos(getAngle())
	 * 		  |			double yVel = getVelocity().getY()+250*Math.sin(getAngle())
	 * 		  |			Bullet newBullet = new Bullet(x, y, xVel, yVel, 3, this, this.getWorld())
	 *        |			getLiveBullets().add(newBullet) )
	 */
	public void fire() {
		if(getLiveBullets().size() < 3) {
			Bullet newBullet = new Bullet(getPosition().getX()+getRadius()*Math.cos(getAngle()), getPosition().getY()+getRadius()*Math.sin(getAngle()),
					getVelocity().getX()+250*Math.cos(getAngle()), getVelocity().getY()+250*Math.sin(getAngle()), 3, this, getWorld());
			getLiveBullets().add(newBullet);
		}
	}
	
	/**
	 * Removes a bullet from the set of bullets of this ship.
	 * 
	 * @param 	bullet
	 * 			The bullet to be removed from the set of bullets of this ship.
	 * @effect	The bullet is removed from the set of bullets of this ship.
	 * 		  | getLiveBullets().remove(bullet)
	 * @throws	IllegalArgumentException("Bullet cannot be null")
	 * 			The given bullet cannot be null.
	 * 		  | bullet == null
	 */
	public void removeBullet(Bullet bullet) throws IllegalArgumentException {
		if(bullet == null)
				throw new IllegalArgumentException("Bullet cannot be null!");
		if(!getLiveBullets().remove(bullet)) {
			throw new IllegalArgumentException("There is no such bullet in the set!");
		}
		getLiveBullets().remove(bullet);
		bullet.setWorld(null);
	}
	
	/**
	 * Get the set of live bullets from this ship.
	 */
	@Basic
	public Set<Bullet> getLiveBullets() {
		return liveBullets;
	}
	
	/**
	 * The Set containing all the live bullets from this ship.
	 */
	private Set<Bullet> liveBullets = new HashSet<Bullet>();
	
	/**
	 * Terminate this ship.
	 * 
	 * @effect	Remove the ship from the set of the world of this ship.
	 * 		  | this.getWorld().remove(this)
	 */
	public void terminate() {
		this.getWorld().remove(this);
	}
	
	/**
	 * Return the current loaded program of this ship.
	*/
	@Basic
	public Program getProgram() {
		return program;
	}
	/**
	 * Set the program of this ship to the given program.
	 * 
	 * @post 	The new program of this ship is the given program.
	 * 		  | new.getProgram() == program
	 * @effect	The new program is set up.
	 * 		  |	getProgram().setup(this);
	 */
	public void setProgram(Program program) {
		if(program == null)
			throw new IllegalArgumentException("Ship cannot be null!");
		this.program = program;
		getProgram().setup(this);
	}
	
	/**
	 * The current loaded program of this ship.
	 */
	private Program program;

	/**
	 * Collide this ship with the given WorldObject other.
	 * 
	 * @param	other
	 * 			The object to collide with.
	 * @effect	The resolve method of the other (WorldObject) is executed.
	 * 		  |	other.resolve(this)
	 */
	@Override
	public void collide(WorldObject other) {
		other.resolve(this);
	}

	/**
	 * Resolve the collision with another ship.
	 * 
	 * @param	other
	 * 			The other ship to resolve collision with.
	 * @effect	The bounceOff method is executed with other as argument.
	 * 		  | bounceOff(other)
	 */
	@Override
	public void resolve(Ship other) {
		bounceOff(other);
	}

	/**
	 * Resolve the collision with an asteroid.
	 * 
	 * @param	other
	 * 			The asteroid to resolve collision with.
	 * @effect	The terminate method is executed.
	 * 		  | this.terminate()
	 */
	@Override
	public void resolve(Asteroid other) {
		this.terminate();
	}

	/**
	 * Resolve the collision with a bullet.
	 * 
	 * @param	other
	 * 			The bullet to resolve collision with.
	 * @effect	If and only if the bullets source isn't this ship,
	 *			then this ship is terminated. Elsewise the bullet is terminated.
	 *		  |	if(other.getSource() != this)
	 *		  |		then(this.terminate())
	 *		  |		else(other.terminate())
	 */
	@Override
	public void resolve(Bullet other) {
		if(other.getSource() != this) {
			this.terminate();
		}
		other.terminate();
	}

	/**
	 * Resolve the collision with an horizontal boundary.
	 * 
	 * @param	hb
	 * 			The horizontal boundary to resolve collision with.
	 * @effect	The ship is bounced of the horizontal boundary.
	 * 		  | bounceHorBound()
	 */
	@Override
	public void resolve(HorBound hb) {
		bounceHorBound();
	}

	/**
	 * Resolve the collision with a vertical boundary.
	 * 
	 * @param	vb
	 * 			The vertical boundary to resolve collision with.
	 * @effect	The ship is bounced of the vertical boundary.
	 * 		  | bounceVerBound()
	 */
	@Override
	public void resolve(VerBound vb) {
		bounceVerBound();
	}

}
